#!/bin/bash
# trap SIGUSR1 as it is being used to check
# for process aliveness when an existing
# pidfile is found
trap ':' USR1
just_started=${just_started:-true}

# We are creating the following directory structure
# /opt/valheim/
#         |___/dl/            <= downloads happen in here
#         |     |___/server/  <= vanilla server download
#         |     |___/plus/    <= ValheimPlus mod download
#         |___/server/        <= vanilla server installation
#         |___/plus/          <= merge of vanilla server and ValheimPlus mod
#
valheim_download_path=/opt/valheim/dl/server    # Valheim server download directory
valheim_install_path=/opt/valheim/server        # Valheim server installation directory
valheim_restartfile="/tmp/valheim.restart"      # Signaling file created by valheim-updater
                                                # or valheim-plus-updater that describes
                                                # if and how to restart the server

# ValheimPlus Mod
vp_updater=/usr/local/bin/valheim-plus-updater
vp_download_path=/opt/valheim/dl/plus           # ValheimPlus download directory
vp_install_path=/opt/valheim/plus               # ValheimPlus installation directory
vp_zipfile=UnixServer.zip                       # Name of the ValheimPlus archive
vp_mergefile="$vp_download_path/merge"          # Signaling file created by valheim-updater
                                                # that tells valheim-plus-updater that Valheim
                                                # server was updated and needs to be merged
                                                # with ValheimPlus
vp_config_path="/config/valheimplus"

# BepInEx Mod
bepinex_updater=/usr/local/bin/bepinex-updater
bepinex_download_path=/opt/valheim/dl/bepinex    # BepInEx download directory
bepinex_install_path=/opt/valheim/bepinex        # BepInEx installation directory
bepinex_config_path="/config/bepinex"
bepinex_mergefile="$bepinex_download_path/merge" # Signaling file created by valheim-updater
                                                 # that tells bepinex-updater that Valheim
                                                 # server was updated and needs to be merged
                                                 # with BepInEx
bepinex_zipfile=BepInEx.zip                      # Name of the BepInEx archive

# Collection of PID files
valheim_server_pidfile=/var/run/valheim/valheim-server.pid
valheim_updater_pidfile=/var/run/valheim/valheim-updater.pid
valheim_backup_pidfile=/var/run/valheim/valheim-backup.pid

# Supervisor config files
supervisor_http_server_conf=/usr/local/etc/supervisor/conf.d/http_server.conf

# Status httpd config files
status_http_server_conf=/usr/local/etc/supervisor/conf.d/status_http_server.conf
status_http_server_updater_conf=/usr/local/etc/supervisor/conf.d/status_http_server_updater.conf

# Commands
cmd_valheim_status=/usr/local/bin/valheim-status
cmd_valheim_is_idle=/usr/local/bin/valheim-is-idle
cmd_valheim_logfilter=/usr/local/bin/valheim-logfilter
cmd_supervisorctl=/usr/local/bin/supervisorctl

# Syslog supervisor config file
supervisor_syslog_conf=/usr/local/etc/supervisor/conf.d/syslog.conf


# File containing the git commit shasum this image was build with
git_commit_file=/usr/local/etc/git-commit.HEAD

# Worlds directories
old_worlds_dir="/config/worlds"
worlds_dir="/config/worlds_local"

# log levels
debug=50
info=40
warn=30
error=20
critical=10
fatal=5
log_level=${log_level:-$debug}


debug()    { logstd $debug    "DEBUG - [$$] - $*"; }
info()     { logstd $info     "INFO - $*"; }
warn()     { logstd $warn     "WARN - $*"; }
error()    { logerr $error    "ERROR - $*"; }
critical() { logerr $critical "CRITIAL - $*"; }
fatal()    { logerr $fatal    "FATAL - $*"; exit 1; }


logstd() {
    local log_at_level
    log_at_level="$1"; shift
    printline "$log_at_level" "$*"
}


logerr() {
    local log_at_level
    log_at_level="$1"; shift
    printline "$log_at_level" "$*" >&2
}


printline() {
    local log_at_level
    local log_data
    log_at_level="$1"; shift
    log_data="$*"

    if [ "$log_at_level" -le "$log_level" ]; then
        echo "$log_data"
    fi
}


ensure_permissions() {
    local restore_errexit=false
    if [ -o errexit ]; then
        restore_errexit=true
        set +e
    fi
    chmod "$CONFIG_DIRECTORY_PERMISSIONS" /config
    chmod -f "$CONFIG_FILE_PERMISSIONS" /config/*.txt
    # Legacy worlds directory
    if [ -d "$old_worlds_dir" ]; then
        chmod "$WORLDS_DIRECTORY_PERMISSIONS" "$old_worlds_dir"
        chmod "$WORLDS_FILE_PERMISSIONS" "${old_worlds_dir}/"*
    fi
    if [ -d "$worlds_dir" ]; then
        chmod "$WORLDS_DIRECTORY_PERMISSIONS" "$worlds_dir"
        chmod "$WORLDS_FILE_PERMISSIONS" "${worlds_dir}/"*
    fi
    if [ "$VALHEIM_PLUS" = true ] && [ -d /config/valheimplus ]; then
        find /config/valheimplus -type d -exec chmod "$VALHEIM_PLUS_CONFIG_DIRECTORY_PERMISSIONS" "{}" +
        find /config/valheimplus -type f -exec chmod "$VALHEIM_PLUS_CONFIG_FILE_PERMISSIONS" "{}" +
    fi
    if [ "$BEPINEX" = true ] && [ -d /config/bepinex ]; then
        find /config/bepinex -type d -exec chmod "$BEPINEX_CONFIG_DIRECTORY_PERMISSIONS" "{}" +
        find /config/bepinex -type f -exec chmod "$BEPINEX_CONFIG_FILE_PERMISSIONS" "{}" +
    fi
    if [ "$restore_errexit" = true ]; then
        set -e
    fi
}


_udp_datagram_count() {
    local datagram_count
    datagram_count="$(nstat | awk '/UdpInDatagrams/{print $2}' | tr -d ' ')"
    echo "${datagram_count:-0}"
}


server_is_crossplay() {
    [[ "${SERVER_ARGS,,}" =~ "-crossplay" ]];
}


server_is_idle() {
    # If server has crossplay enabled, A2S query performed by valheim-status always
    # returns 0 players online. So we use nstat to count UDP datagrams instead.
    if server_is_crossplay || [ "$SERVER_PUBLIC" = 0 ]; then
        # Throw away datagram statistics since last run
        _udp_datagram_count &>/dev/null
        # Wait to track datagrams over window
        sleep "$IDLE_DATAGRAM_WINDOW"
        if [ "$(_udp_datagram_count)" -gt "$IDLE_DATAGRAM_MAX_COUNT" ]; then
            return 1
        else
            return 0
        fi
    else
        "$cmd_valheim_status" > /dev/null 2>&1
    fi
}


server_is_running() {
    test "$(supervisorctl status valheim-server | awk '{print $2}')" = RUNNING
}


server_is_listening() {
    # Check if server is listening on either server or query port
    # since crossplay-enabled servers don't listen on $SERVER_PORT
    
    awk -v server_port="$SERVER_PORT" -v server_query_port="$SERVER_QUERY_PORT" '
        BEGIN {
            exit_code = 1
        }
        {
            if ($1 ~ /^[0-9]/) {
                split($2, local_bind, ":")
                listening_port = sprintf("%d", "0x" local_bind[2])
                if (listening_port == server_query_port || listening_port == server_port) {
                    exit_code = 0
                    exit
                }
            }
        }
        END {
            exit exit_code
        }
    ' /proc/net/udp*
}


check_lock() {
    local pidfile
    local predecessor_pid
    local numre
    pidfile=$1
    predecessor_pid=$(<"$pidfile")
    numre='^[0-9]+$'
    if [[ "$predecessor_pid" =~ $numre ]] ; then
        debug "Sending SIGUSR1 to PID $predecessor_pid"
        if kill -USR1 "$predecessor_pid" &> /dev/null; then
            fatal "Process with PID $predecessor_pid already running - exiting"
        else
            info "Removing stale PID file and starting run"
            clear_lock_and_run "$pidfile"
        fi
    else
        warn "Predecessor PID is corrupt - clearing lock and running"
        clear_lock_and_run "$pidfile"
    fi
}


clear_lock_and_run() {
    local pidfile
    pidfile=$1
    clear_lock "$pidfile"
    main
}


clear_lock() {
    local pidfile
    pidfile=$1
    info "Releasing PID file $pidfile"
    rm -f "$1"
}


error_handler() {
    local ec
    local line_no
    local func_call_line
    local command
    local stack
    ec=$1
    line_no=$2
    func_call_line=$3
    command="$4"
    stack="$5"
    error "Error in line $line_no command '$command' exited with code $ec in $stack called in line $func_call_line"
    return "$ec"
}


write_valheim_plus_config() {
    if [ -d "$vp_config_path" ]; then
        debug "Writing ValheimPlus config"
        if env | grep "^$VALHEIM_PLUS_CFG_ENV_PREFIX" > /dev/null; then
            /usr/local/bin/env2cfg --verbose --config "$vp_config_path/valheim_plus.cfg" --env-prefix "$VALHEIM_PLUS_CFG_ENV_PREFIX"
        fi
    fi
}


write_bepinex_config() {
    local config_path=$1
    local plugins_path=$2
    if [ -n "$PRE_BEPINEX_CONFIG_HOOK" ]; then
        info "Running pre BepInEx config hook: $PRE_BEPINEX_CONFIG_HOOK"
        eval "$PRE_BEPINEX_CONFIG_HOOK"
    fi
    if [ -d "$config_path" ]; then
        debug "Writing BepInEx config"
        if env | grep "^$BEPINEX_CFG_ENV_PREFIX" > /dev/null; then
            /usr/local/bin/env2cfg --verbose --config "$config_path/BepInEx.cfg" --env-prefix "$BEPINEX_CFG_ENV_PREFIX"
        fi
        if [ -d "$config_path/plugins" ] && [ -d "$plugins_path" ]; then
            info "Syncing BepInEx plugins from $config_path/plugins/ -> $plugins_path"
            rsync -a --itemize-changes "$config_path/plugins/" "$plugins_path"
        fi
    fi
    if [ -n "$POST_BEPINEX_CONFIG_HOOK" ]; then
        info "Running post BepInEx config hook: $POST_BEPINEX_CONFIG_HOOK"
        eval "$POST_BEPINEX_CONFIG_HOOK"
    fi
}


write_restart_file() {
    local mode
    local reason
    reason=$1
    if [ "$just_started" = true ] && [ "$reason" = just_started ]; then
        mode="start"
    else
        mode="restart"
    fi
    if [ ! -f "$valheim_restartfile" ]; then
        debug "Writing file to $mode Valheim server"
        echo "$mode" > "$valheim_restartfile"
    fi
}


update_server_status() {
    local status
    status=$1
    echo "$status" > "$SERVER_STATUS_FILE"
}


write_bannedlist() {
    write_serverlist banned "$BANNEDLIST_IDS"
}


write_adminlist() {
    write_serverlist admin "$ADMINLIST_IDS"
}


write_permittedlist() {
    write_serverlist permitted "$PERMITTEDLIST_IDS"
}


write_serverlist() {
    local type
    local id_list
    local list_file
    type=$1
    id_list=$2
    list_file="/config/${type}list.txt"

    if [ -n "$id_list" ]; then
        debug "Writing $list_file"
        # Retain original file comment, including weird double space
        echo "// List $type players ID  ONE per line" > "$list_file"
        # shellcheck disable=SC2001
        echo "$id_list" | sed -e "s/ \\+/\\n/g" >> "$list_file"
    fi
}


extract_archive() {
    local archive_path="$1"
    local archive_file="$2"
    cd "$archive_path" || fatal "Could not cd $archive_path"
    debug "Extracting downloaded ZIP archive"
    rm -rf extracted
    mkdir -p extracted
    unzip -d extracted/ "$archive_file"
}


download_mod() {
    local download_url="$1"
    local updated_at="$2"
    local download_path="$3"
    debug "Downloading $download_url to $download_path"
    curl -sfSL -o "$download_path" "$download_url" \
        && echo "$updated_at" > "$download_path.updated_at"
}


check_merge() {
    local mergefile=$1
    local download_path=$2
    local zipfile=$3
    local install_path=$4
    local config_path=$5
    local extraction_path=$6
    local mod_name=$7

    # The control file $mergefile is either created
    # in prepare_mod() if the mod is being installed for
    # the first time or an update is available, or
    # it is created by valheim-updater if a Valheim server update
    # was downloaded and the mod needs to be applied to it.
    if [ -f "$mergefile" ]; then
        info "Valheim dedicated server or $mod_name mod got updated - extracting and merging installation files"
        (set -e; extract_archive "$download_path" "$zipfile" && merge_mod "$install_path" "$config_path" "$download_path/$extraction_path")
        # shellcheck disable=SC2181
        if [ $? -eq 0 ]; then
            debug "Successfully installed $mod_name mod"
            cp -f "$zipfile.updated_at" "$zipfile.installed_at"
            rm -f "$mergefile"
        else
            error "Failed to extract and install $mod_name - retrying later"
        fi
    fi
}


prepare_mod() {
    local download_url="$1"
    local updated_at="$2"
    local mergefile="$3"
    local zipfile="$4"
    download_mod "$download_url" "$updated_at" "$zipfile" \
    && touch "$mergefile"
}


merge_mod() {
    local mod_install_path="$1"
    local mod_config_path="$2"
    local mod_download_path="$3"
    local config_file
    local pkg_config_dir
    local dest_file
    mod_config_path=${mod_config_path%/}
    mod_download_path=${mod_download_path%/}
    debug "Merging Valheim server and mod"
    # remove any old install directories
    rm -rf "$mod_install_path.tmp" "$mod_install_path.old"
    # create a new install directory where we will stage the new version
    mkdir -p "$mod_install_path.tmp"
    # rsync all Valheim dedicated server files
    rsync -a --itemize-changes --exclude server_exit.drp --exclude steamapps "$valheim_download_path/" "$mod_install_path.tmp"
    # rsync all mod files on top of the dedicated server files
    rsync -a --itemize-changes "$mod_download_path/" "$mod_install_path.tmp"
    # if /config/<modname>/ does not exist copy the default config from the ZIP archive
    debug "Ensuring $mod_config_path/plugins exists"
    mkdir -p "$mod_config_path/plugins"
    pkg_config_dir="$mod_install_path.tmp/BepInEx/config"
    if [ -d "$pkg_config_dir" ]; then
        cd "$pkg_config_dir" || fatal "Could not cd $pkg_config_dir"
        for config_file in *; do
            dest_file="$mod_config_path/$config_file"
            debug "Storing $config_file as $dest_file.default"
            # always copy the configs that came with the latest ZIP archive to .cfg.default
            cp -f "$config_file" "$dest_file.default"
            if [ ! -f "$dest_file" ]; then
                debug "Config $dest_file does not exist - copying from archive"
                cp -f "$config_file" "$dest_file"
            fi
            if [ "$config_file" = "valheim_plus.cfg" ]; then
                write_valheim_plus_config
            elif [ "$config_file" = "BepInEx.cfg" ]; then
                write_bepinex_config "$mod_config_path" "$mod_install_path.tmp/BepInEx/plugins"
            fi
        done
        cd - || fatal "Could not cd -"
    fi
    # ensure config file permissions
    ensure_permissions
    # remove the config folder within the server directory and symlink it to /config
    debug "Removing $pkg_config_dir and symlinking from $mod_config_path"
    rm -rf "$pkg_config_dir"
    ln -s "$mod_config_path" "$pkg_config_dir"
    # move an existing copy of ValheimPlus to the .old extension
    if [ -d "$mod_install_path" ]; then
        debug "Moving old $mod_install_path -> $mod_install_path.old"
        mv -f "$mod_install_path" "$mod_install_path.old"
    fi
    # move the staging folder to the live folder and signal valheim-updater to restart the server
    debug "Moving $mod_install_path.tmp -> $mod_install_path"
    mv "$mod_install_path.tmp" "$mod_install_path"
    write_restart_file updated
}


check_for_mod_update() {
    local download_url="$1"
    local remote_updated_at="$2"
    local zipfile="$3"
    local mergefile="$4"
    local download_path="$5"
    local config_path="$6"
    local extraction_path="$7"
    local install_path="$8"
    local mod_name="$9"
    local local_updated_at
    local local_installed_at

    mkdir -p "$download_path" "$install_path"
    cd "$download_path" || fatal "Could not cd $download_path"
    if [ -f "$zipfile" ] && [ -f "$zipfile.updated_at" ] && [ -f "$zipfile.installed_at" ]; then
        local_updated_at=$(< "$zipfile.updated_at")
        local_installed_at=$(< "$zipfile.installed_at")
        if [ "$local_updated_at" = "$remote_updated_at" ] && [ "$local_updated_at" = "$local_installed_at" ]; then
            debug "Local $mod_name archive is identical to remote archive and was successfully installed - no update required"
        else
            info "Local $mod_name archive with update date $local_updated_at differs from remote date $remote_updated_at or failed to successfully install - updating"
            prepare_mod "$download_url" "$remote_updated_at" "$mergefile" "$download_path/$zipfile"
        fi
    else
        info "Fresh $mod_name install"
        prepare_mod "$download_url" "$remote_updated_at" "$mergefile" "$download_path/$zipfile"
    fi

    check_merge "$mergefile" "$download_path" "$zipfile" "$install_path" "$config_path" "$extraction_path" "$mod_name"
}


iec_size_format() {
    local byte_size=$1
    local use_bc=false
    local float_regex="^([0-9]+\\.?[0-9]*)\$"

    if [ -z "$byte_size" ] || ! [[ "$byte_size" =~ $float_regex ]]; then
        error "Input $byte_size is no valid float"
        return 1
    fi
    if command -v bc > /dev/null 2>&1; then
        use_bc=true
    fi
    for unit in B KiB MiB GiB TiB PiB EiB ZiB; do
        if [ "${byte_size%.*}" -lt 1024 ]; then
            printf "%.2f %s\\n" "$byte_size" "$unit"
            return
        fi
        if [ "$use_bc" = true ]; then
            byte_size=$(echo "$byte_size/1024" | bc -l)
        else
            byte_size=$((byte_size/1024))
        fi
    done
    printf "%.2f YiB\\n" $byte_size
}
